﻿using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace AChance.Func
{
    public static class FunLock
    {
        /// <summary>
        /// 加锁模式
        /// </summary>
        public enum Mode
        {
            /// <summary>
            /// 不加锁
            /// </summary>
            No,
            /// <summary>
            /// 本地锁
            /// </summary>
            Local,
            /// <summary>
            /// 分布式锁
            /// </summary>
            Distributed
        }
        public static class Span
        {
            public const double EXPIRY = 10000;
            public const double WAIT = 3000;
            public const double RETRY = 200;
        }
        /// <summary>
        /// 加锁
        /// </summary>
        public static bool Lock(Mode mode, string key, double expiry = Span.EXPIRY, double wait = Span.WAIT, double retry = Span.RETRY, string lockFailMsg = null)
        {
            return mode switch
            {
                Mode.Local => Local.Lock(key, expiry, wait, retry, lockFailMsg),
                Mode.Distributed => Distributed.Lock(key, expiry, wait, retry, lockFailMsg),
                _ => true
            };
        }
        /// <summary>
        /// 解锁
        /// </summary>
        public static bool Unlock(Mode mode, string key)
        {
            return mode switch
            {
                Mode.Local => Local.Unlock(key),
                Mode.Distributed => Distributed.Unlock(key),
                _ => true
            };
        }
        /// <summary>
        /// 本地锁
        /// </summary>
        public static class Local
        {
            private static IMemoryCache _locks = new MemoryCache(Options.Create(new MemoryCacheOptions()));
            private static readonly object _lockObj = new object();
            public static bool Lock(string key, double expiry = Span.EXPIRY, double wait = Span.WAIT, double retry = Span.RETRY, string lockFailMsg = null)
            {
                var waitTime = DateTime.Now + TimeSpan.FromMilliseconds(wait);
                var expirySpan = TimeSpan.FromMilliseconds(expiry.IsMoreZero() ? expiry : Span.EXPIRY);
                var value = string.Empty;
                do
                {
                    lock (_lockObj)
                    {
                        if (!_locks.TryGetValue(key, out _))
                        {
                            _locks.Set(key, value, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirySpan));
                            return true;
                        }
                    }
                    Thread.Sleep((int)retry);
                } while (DateTime.Now < waitTime);
                return lockFailMsg.IsNullOrEmpty() ? false : throw new Exception(lockFailMsg);
            }
            public static bool Unlock(string key)
            {
                _locks.Remove(key);
                return true;
            }
        }
        /// <summary>
        /// 分布式锁
        /// </summary>
        public static class Distributed
        {
            public static bool Lock(string key, double expiry = Span.EXPIRY, double wait = Span.WAIT, double retry = Span.RETRY, string lockFailMsg = null)
            {
                var waitTime = DateTime.Now + TimeSpan.FromMilliseconds(wait);
                var expirySpan = TimeSpan.FromMilliseconds(expiry.IsMoreZero() ? expiry : Span.EXPIRY);
                var value = Guid.NewGuid().ToString();
                do
                {
                    if (FunRedis.Db.Locks.LockTake(nameof(Lock), value, TimeSpan.FromSeconds(5)))
                    {
                        try
                        {
                            if (FunRedis.Db.Locks.StringSet(key, value, expirySpan, When.NotExists))
                                return true;
                            else if (FunRedis.Db.Locks.KeyTimeToLive(key) is null)
                                return FunRedis.Db.Locks.KeyExpire(key, expirySpan);
                        }
                        finally
                        {
                            FunRedis.Db.Locks.LockRelease(nameof(Lock), value);
                        }
                    }
                    Thread.Sleep((int)retry);
                } while (DateTime.Now < waitTime);
                return lockFailMsg.IsNullOrEmpty() ? false : throw new Exception(lockFailMsg);
            }
            public static bool Unlock(string key)
            {
                FunRedis.Db.Locks.KeyDelete(key);
                return true;
            }
        }


    }
}
